/*
 * ﻿Copyright (C) 2013 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.eatestutil.ejbtestutil;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.Metamodel;

/**
 * EJB test helper abstract class as merely <code>EntityManager</code> wrapper in order to 
 * use <code>EntityManager</code> instance from outside of EJB beans managed by EJB container.<br />
 * Sub-class must be annotated with either <code>&#x40;Stateless</code> or <code>&#x40;Statefull</code> 
 * annotation (and other necessary annotation of course), and must override 
 * <code>{@link #getEntityManager()}</code> method.
 * 
 * @author Arata Y.
 */
public abstract class AbstractEntityManagerFacadeImpl implements EntityManagerFacade {
	/**
	 * Sub-class must override this to provide <code>EntityManager</code> instance managed by 
	 * EJB container.
	 * 
	 * @return <code>EntityManager</code> managed by EJB container.
	 */
	public abstract EntityManager getEntityManager();

	private Map<String, Set<Method>> entityManagerMethodMap;
		protected Map<String, Set<Method>> getEntityManagerMethodMap() {
			if ( entityManagerMethodMap == null) {
				entityManagerMethodMap = new HashMap<String, Set<Method>>();
				
				Method[] entityManagerMethods = EntityManager.class.getDeclaredMethods();
				for( Method method : entityManagerMethods) {
					String methodName = method.getName();
					Set<Method> methodSet = entityManagerMethodMap.get( methodName);
						if ( methodSet == null) {
							methodSet = new HashSet<Method>();
							entityManagerMethodMap.put( methodName, methodSet);
						}
					methodSet.add( method);
				}
			}
			return entityManagerMethodMap;
		}
	
	protected Object invokeEntityManager( Object... args) {
		final StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[ 2];
		final String methodName = stackTraceElement.getMethodName();
		
		final Set<Method> methodSet = getEntityManagerMethodMap().get( methodName);
			if ( methodSet.size() < 1) {
				throw new UnsupportedOperationException(
						String.format(
								"Could not find %1$s method for arguments of %2$s in %3$s class.",
								methodName,
								Arrays.toString( args), 
								EntityManager.class.getSimpleName())
						);
			}
			
		Method method = null;
			Iterator<Method> iterator = methodSet.iterator();
			while( iterator.hasNext()) {
				Method methodObj = iterator.next();
					Class<?>[] parameterTypes = methodObj.getParameterTypes();
						if ( args.length != parameterTypes.length) continue;
					boolean isParameterMatched = true;
						for( int index = 0; index < parameterTypes.length; index++) {
							if ( !parameterTypes[ index].isInstance( args[ index])) {
								isParameterMatched = false;
								break;
							}
						}
					if ( !isParameterMatched) continue;
				method = methodObj;
				break;
			}
			if ( method == null) {
				throw new UnsupportedOperationException(
						String.format(
								"Could not find %1$s method for arguments of %2$s in %3$s class.",
								methodName,
								Arrays.toString( args), 
								EntityManager.class.getSimpleName())
						);
			}
			
		try {
			return method.invoke( getEntityManager(), args);
		}
		catch( Exception exception) {
			throw new RuntimeException( exception);
		}
	}
	
	// EntityManager interface implementation -----------------------------------------------------
	public void clear() {
		invokeEntityManager();
	}
	public void close() {
		invokeEntityManager();
	}
	public boolean contains( final Object arg0) {
		return (Boolean)invokeEntityManager( arg0);
	}
	public void detach( final Object arg0) {
		invokeEntityManager( arg0);
	}
	@SuppressWarnings( value = { "unchecked"})
	public <T> T find(
			final Class<T> arg0, final Object arg1, 
			final LockModeType arg2, final Map<String, Object> arg3) {
		return (T)invokeEntityManager( arg0, arg1, arg2, arg3);
	}
	@SuppressWarnings( value = { "unchecked"})
	public <T> T find( final Class<T> arg0, final Object arg1, final LockModeType arg2) {
		return (T)invokeEntityManager( arg0, arg1, arg2);
	}
	@SuppressWarnings( value = { "unchecked"})
	public <T> T find( final Class<T> arg0, final Object arg1, final Map<String, Object> arg2) {
		return (T)invokeEntityManager( arg0, arg1, arg2);
	}
	@SuppressWarnings( value = { "unchecked"})
	public <T> T find( final Class<T> arg0, final Object arg1) {
		return (T)invokeEntityManager( arg0, arg1);
	}
	public void flush() {
		invokeEntityManager();
	}
	public EntityManagerFactory getEntityManagerFactory() {
		return (EntityManagerFactory)invokeEntityManager();
	}
	public LockModeType getLockMode( final Object arg0) {
		return (LockModeType)invokeEntityManager( arg0);
	}
	public Metamodel getMetamodel() {
		return (Metamodel)invokeEntityManager();
	}
	public EntityTransaction getTransaction() {
		return (EntityTransaction)invokeEntityManager();
	}
	public boolean isJoinedToTransaction() {
		return (Boolean)invokeEntityManager();
	}
	public boolean isOpen() {
		return (Boolean)invokeEntityManager();
	}
	public void joinTransaction() {
		invokeEntityManager();
	}
	public void lock( final Object arg0, final LockModeType arg1, final Map<String, Object> arg2) {
		invokeEntityManager( arg0, arg1, arg2);
	}
	public void lock( final Object arg0, final LockModeType arg1) {
		invokeEntityManager( arg0, arg1);
	}
	@SuppressWarnings( value = { "unchecked"})
	public <T> T merge( final T arg0) {
		return (T)invokeEntityManager( arg0);
	}
	public void persist( final Object arg0) {
		invokeEntityManager( arg0);
	}
	public void refresh( final Object arg0, final LockModeType arg1, final Map<String, Object> arg2) {
		invokeEntityManager( arg0, arg1, arg2);
	}
	public void refresh( final Object arg0, final LockModeType arg1) {
		invokeEntityManager( arg0, arg1);
	}
	public void refresh( final Object arg0, final Map<String, Object> arg1) {
		invokeEntityManager( arg0, arg1);
	}
	public void refresh( final Object arg0) {
		invokeEntityManager( arg0);
	}
	public void remove( final Object arg0) {
		invokeEntityManager( arg0);
	}
	public void setFlushMode( final FlushModeType arg0) {
		invokeEntityManager( arg0);
	}
	public void setProperty( final String arg0, final Object arg1) {
		invokeEntityManager( arg0, arg1);
	}
	@SuppressWarnings( value = { "unchecked"})
	public <T> TypedQuery<T> createNamedQuery( final String arg0, final Class<T> arg1) {
		return (TypedQuery<T>)invokeEntityManager( arg0, arg1);
	}
	public Query createNamedQuery( final String arg0) {
		return (Query)invokeEntityManager( arg0);
	}
	public Query createNativeQuery( final String arg0, @SuppressWarnings("rawtypes") final Class arg1) {
		return (Query)invokeEntityManager( arg0, arg1);
	}
	public Query createNativeQuery( final String arg0, final String arg1) {
		return (Query)invokeEntityManager( arg0, arg1);
	}
	public Query createNativeQuery( final String arg0) {
		return (Query)invokeEntityManager( arg0);
	}
	@SuppressWarnings( value = { "unchecked"})
	public <T> TypedQuery<T> createQuery( final CriteriaQuery<T> arg0) {
		return (TypedQuery<T>)invokeEntityManager( arg0);
	}
	@SuppressWarnings("unchecked")
	public <T> TypedQuery<T> createQuery( final String arg0, final Class<T> arg1) {
		return (TypedQuery<T>)invokeEntityManager( arg0, arg1);
	}
	public Query createQuery( final String arg0) {
		return (Query)invokeEntityManager( arg0);
	}
	public CriteriaBuilder getCriteriaBuilder() {
		return (CriteriaBuilder)invokeEntityManager();
	}
	public Object getDelegate() {
		return invokeEntityManager();
	}
	public FlushModeType getFlushMode() {
		return (FlushModeType)invokeEntityManager();
	}
	@SuppressWarnings("unchecked")
	public Map<String, Object> getProperties() {
		return (Map<String, Object>)invokeEntityManager();
	}
	@SuppressWarnings("unchecked")
	public <T> T getReference( final Class<T> arg0, final Object arg1) {
		return (T)invokeEntityManager( arg0, arg1);
	}
	@SuppressWarnings("unchecked")
	public <T> T unwrap( final Class<T> arg0) {
		return (T)invokeEntityManager( arg0);
	}
	// --------------------------------------------------------------------------------------------
	
	/**
	 * {@inheritDoc}
	 * @throws IllegalArgumentException
	 * @throws SecurityException
	 */
	@Override
	public Object runExternalMethod( 
			final Object externalObj, final Method externalMethod, final Object... args) 
	throws IllegalAccessException, InvocationTargetException {
		externalMethod.setAccessible( true);
		return externalMethod.invoke( externalObj, args);
	}
}
